# Ключевое слово await
# Наибольшее количество ошибок начинающих программистов в асинхронном программировании - это некорректное использование ключевого
# слова await. В данном разделе соберем вместе то, что мы уже упоминалось в данном курсе касательно await, а также расскажем про
# другие возможности await.

# Правила и особенности использования await.
# 1. await - это ключевое слово, которое используется для переключения между awaitable объектами. Основная задача
# асинхронного программирования - конкурентное выполнение задач. Это возможно благодаря await. Когда интерпретатор Python
# встречает ключевое слово await, он приостанавливает выполнение текущей функции и передает управление обратно в цикл событий. 
# import asyncio

# async def my_coroutine(num):
#     print(f'Начинаем выполнение корутины {num}')
#     await asyncio.sleep(1)  # в этот момент данная корутина передает управление в цикл событий
#     print(f'Закончили выполнение корутины {num}')

# async def main():
#     tasks = [asyncio.create_task(my_coroutine(i)) for i in range(1, 6)]
#     await asyncio.gather(*tasks)

# asyncio.run(main())
# В данном примере каждая задача, созданная из корутины my_coroutine(), передает управление обратно в цикл событий
# на строке await asyncio.sleep(1). Именно await дает сигнал на переключение контекста. Ранее мы говорили, что в
# asyncio реализована кооперативная многозадачность, в которой корутины/задачи сами передают управление. 
# Для этого используется именно ключевое слово await. 
######################################################
# 2. await нельзя использовать с не awaitable объектами. При попытке использовать await с объектами, не поддерживающими ожидание,
# т.е. не с awaiable объектами, мы получим ошибку TypeError.
# НЕЛЬЗЯ
# import asyncio

# async def main():
#     await None  # или await "Hello, World!" и др. не awaitable объекты

# asyncio.run(main())  # ошибка TypeError!
# await используется только с корутинами, задачами или объектами Future, а также встроенными функциями asyncio и 
# совместимых модулей, поддерживающих ожидание, например asyncio.sleep().
#######################################################
# 3. await нельзя использовать вне корутин. 

# НЕЛЬЗЯ
# import asyncio

# def main():
#     await asyncio.sleep(1)

# asyncio.run(main())  # ошибка SyntaxError!
##########################################################
# await блокирует выполнение корутины. Несмотря на то, что именно await дает возможность конкурентного выполнения задач,
# ожидание является блокирующей операцией внутри текущей корутины. При этом другие корутины могут продолжать свою работу. 

# import asyncio

# async def my_coroutine():
#     print(f'Начинаем')
#     await asyncio.sleep(1)  # Тут блокируется корутина my_coroutine() до завершения asyncio.sleep(). 
#     print(f'Закончили')

# async def main():
#     await my_coroutine()  #  Тут блокируется main() до завершения my_coroutine().
#     print('выполнение корутины завершено') 


# asyncio.run(main())
# Вывод: 

# Начинаем
# Закончили
# выполнение корутины завершено

# В данном примере выполнение корутины my_coroutine() блокируется на 1 секунду на строчкеawait asyncio.sleep(1).
# Блокировка текущей корутины закончится, когда asyncio.sleep(1) вернет результат. После этого корутина может продолжить выполнение. 

# Выполнение основной корутины main() также блокируется на строчкеawait my_coroutine(), а блокировка завершается
# когда выполнение ожидаемой корутины завершается.  

# При этом внутри цикла событий идет переключение контекста и могут выполняться другие задачи, но каждая
# конкретная корутина блокируется до завершения выполнения операции с ключевым словом await.
######################################
# 5. await возвращает результат. Так как await блокирует выполнение корутины и дожидается выполнения операции - он же
# возвращает результат. Ранее мы использовали для получения результата функцию result(), но можем использовать и await.
# import asyncio

# async def my_coroutine():
#     await asyncio.sleep(1)
#     return "Задача завершена"

# async def main():
#     result = await my_coroutine()  # сохраняем в переменную результат выполнения my_coroutine() 
#     print(result)


# asyncio.run(main())

#await asymcio.gather(*tasks) дожидается выполнения всех задач возвращает список результатов всех задач.

# import asyncio

# async def my_coroutine(num):
#     await asyncio.sleep(1)
#     return f"Задача {num} завершена"

# async def main():
#     tasks = [asyncio.create_task(my_coroutine(i)) for i in range(1, 4)]
#     result = await asyncio.gather(*tasks)
#     print(result)

# asyncio.run(main())
# Вывод: 

# ['Задача 1 завершена', 'Задача 2 завершена', 'Задача 3 завершена']
#https://ucarecdn.com/bcac592b-bf84-441a-b34e-2580e2b33663/


# 6. Корутина должна содержать await. 
# Мы можем написать корутину, в которой не будет ключевого слова await и ошибок не будет.
# Но при такой реализации выполняемая корутина не сможет передать управление, следовательно асинхронная
# функция отработает как обычная синхронная функция. Выполнение других задач начнется после полного завершения
# корутины. Для демонстрации такого случая рассмотрим пример, в котором вместо поддерживающей ожидание
# функции asyncio.sleep() будет использована синхронная time.sleep().
# import asyncio
# import time

# async def my_coroutine_without_await(num):
#     print(f'Начинаем выполнение корутины {num}')
#     time.sleep(1)  
#     print(f'Закончили выполнение корутины {num}')

# async def main():
#     tasks = [asyncio.create_task(my_coroutine_without_await(i)) for i in range(1, 6)]
#     await asyncio.gather(*tasks)

# asyncio.run(main())
# Несмотря на то, что в данном примере мы запускаем выполнение всех задач конкурентно, ни одна задача не может передать контроль
# обратно в цикл событий и все задачи выполняются по очереди. Чтобы ваш асинхронный код работал эффективно - в теле корутин
# должны быть преимущественно операции, поддерживающие ожидание. Несмотря на то, что асинхронное программирование идеально
# для конкурентного выполнения IO задач, обязательно надо использовать асинхронные версии функций, реализующих IO операции.
# При использовании синхронных функций библиотеки requests или синхронных функций для работы с файлами (например менеджер контекста open())
# мы получим тот же результат, что и в примере выше.

# Поэтому ранее во всех учебных примерах мы использовали асинхронную встроенную функцию asyncio.sleep() для имитации длительной
# операции. Асинхронные версии функций для работы с файлами и для сетевых запросов будут рассмотрены в следующих модулях курса. 

# Осторожность при использовании await:
# При использовании await необходимо быть осторожным, чтобы избежать неожиданных ошибок. 

# Ошибки выполнения: если асинхронная функция, которую вы ожидаете, выбрасывает исключение, await также выбросит это исключение.
# Как обрабатывать подобные исключения, мы будем говорить в следующих темах.

# Задержки в выполнении кода: следует помнить, что использование await может привести к задержкам, так как он останавливает 
# выполнение текущей корутины, пока не будет получен результат выполнения указанной асинхронной операции.  

#Пример использования await для последовательных операций:  
import asyncio

# Асинхронная функция, имитирующая чтение данных из файла
async def read_data_from_file(filename):
    print(f"Начинаем чтение из файла {filename}")
    await asyncio.sleep(2)  # # Имитация задержки для чтения файла
    print(f"Чтение из файла {filename} завершено")
    return f"данные из {filename}"

# Асинхронная функция, имитирующая отправку данных в интернет
async def send_data_to_internet(data):
    print("Начинаем отправку данных в интернет")
    await asyncio.sleep(3)  # Имитация задержки для отправки данных
    print("Отправка данных в интернет завершена")

# Главная асинхронная функция, которая управляет выполнением программы
async def main():
    filename = "example.txt"
    # Чтение данных из файла
    file_data = await read_data_from_file(filename)
    # Отправка прочитанных данных в интернет
    await send_data_to_internet(file_data)

asyncio.run(main())

# Вывод:

# Начинаем чтение из файла example.txt
# Чтение из файла example.txt завершено
# Начинаем отправку данных в интернет
# Отправка данных в интернет завершена

# В этом примере:

# Сначала вызывается функция read_data_from_file(), которая начинает процесс чтения данных из файла.
# Функция ждет, пока не будет выполнено чтение, имитируемое с помощью await asyncio.sleep(2). 
# Данные сохраняются в переменную file_data с помощью await.
# Как только чтение файла завершено, программа продолжает работу и переходит к следующей задаче - отправке
# данных в интернет с помощью функции send_data_to_internet(). В качестве аргумента передаются данные,
# полученные после выполнения предыдущей функции. 
# В функции send_data_to_internet() также используется await для имитации задержки отправки данных.
# Использование await позволяет вашей программе "паузить" выполнение корутины и переключиться на выполнение
# других задач, если таковые имеются. В нашем случае, между этапами чтения файла и отправки данных программа
# могла бы выполнять другие операции, если бы они были заданы. 